/*
 Copyright (c) 2013 yvt,
 based on code of pysnip (c) Mathias Kaerlev 2011-2012.

 This file is part of OpenSpades.

 OpenSpades is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 OpenSpades is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with OpenSpades.  If not, see <http://www.gnu.org/licenses/>.

 */

#include "Player.h"

#include "GameMap.h"
#include "GameMapWrapper.h"
#include "Grenade.h"
#include "HitTestDebugger.h"
#include "IWorldListener.h"
#include "PhysicsConstants.h"
#include "Weapon.h"
#include "World.h"
#include <Core/Debug.h>
#include <Core/Exception.h>
#include <Core/Settings.h>

namespace spades {
	namespace client {

		Player::Player(World &w, int playerId, WeaponType wType, int teamId, Vector3 position,
		               IntVector3 color)
		    : world(w) {
			SPADES_MARK_FUNCTION();

			lastClimbTime = -100;
			lastJumpTime = -100;
			lastJump = false;
			tool = ToolWeapon;
			airborne = false;
			wade = false;
			this->position = position;
			velocity = MakeVector3(0, 0, 0);
			orientation = MakeVector3(1, 0, 0);
			if (teamId) // quick hack for correct spawn orientation
				orientation = MakeVector3(-1, 0, 0);
			eye = MakeVector3(0, 0, 0);
			moveDistance = 0.f;
			moveSteps = 0;

			this->playerId = playerId;
			this->weapon.reset(Weapon::CreateWeapon(wType, *this, *w.GetGameProperties()));
			this->weaponType = wType;
			this->teamId = teamId;
			this->weapon->Reset();
			this->color = color;

			health = 100;
			grenades = 3;
			blockStocks = 50;
			blockColor = IntVector3::Make(111, 111, 111);

			nextSpadeTime = 0.f;
			nextDigTime = 0.f;
			nextGrenadeTime = 0.f;
			nextBlockTime = 0.f;
			firstDig = false;
			lastReloadingTime = 0.f;

			pendingPlaceBlock = false;
			pendingRestockBlock = false;

			blockCursorActive = false;
			blockCursorDragging = false;

			holdingGrenade = false;
			reloadingServerSide = false;
			canPending = false;
		}

		Player::~Player() { SPADES_MARK_FUNCTION(); }

		bool Player::IsLocalPlayer() { return world.GetLocalPlayer() == this; }

		void Player::SetInput(PlayerInput newInput) {
			SPADES_MARK_FUNCTION();

			if (!IsAlive())
				return;

			if (newInput.crouch != input.crouch) {
				if (newInput.crouch) {
					if (!airborne) {
						position.z += 0.9f;
					}
				} else {
					// Refuse the standing-up request if there's no room
					if (!TryUncrouch()) {
						// ... But if the request is from the server,
						// don't ask questions
						if (IsLocalPlayer()) {
							newInput.crouch = true;
						}
					}
				}
			}

			input = newInput;
		}

		void Player::SetWeaponInput(WeaponInput newInput) {
			SPADES_MARK_FUNCTION();
			auto *listener = GetWorld().GetListener();

			if (!IsAlive())
				return;

			if (input.sprint && !input.crouch &&
			    (input.moveBackward || input.moveForward || input.moveLeft || input.moveRight)) {
				newInput.primary = false;
				newInput.secondary = false;
			}
			if (tool == ToolSpade) {
				if (newInput.secondary)
					newInput.primary = false;
				if (newInput.secondary != weapInput.secondary) {
					if (newInput.secondary) {
						nextDigTime = world.GetTime() + 1.f;
						firstDig = true;
					}
				}
			} else if (tool == ToolGrenade) {
				if (world.GetTime() < nextGrenadeTime) {
					newInput.primary = false;
				}
				if (grenades == 0) {
					newInput.primary = false;
				}
				if (weapInput.primary && holdingGrenade && GetGrenadeCookTime() < .15f) {
					// pin is not pulled yet
					newInput.primary = true;
				}
				if (newInput.primary != weapInput.primary) {
					if (!newInput.primary) {
						if (holdingGrenade) {
							nextGrenadeTime = world.GetTime() + .5f;
							ThrowGrenade();
						}
					} else {
						holdingGrenade = true;
						grenadeTime = world.GetTime();
						if (listener && this == world.GetLocalPlayer())
							// playing other's grenade sound
							// is cheating
							listener->LocalPlayerPulledGrenadePin();
					}
				}
			} else if (tool == ToolBlock) {
				// work-around for bug that placing block
				// occasionally becomes impossible
				if (nextBlockTime >
				    world.GetTime() + std::max(GetToolPrimaryDelay(), GetToolSecondaryDelay())) {
					nextBlockTime =
					  world.GetTime() + std::max(GetToolPrimaryDelay(), GetToolSecondaryDelay());
				}

				if (world.GetTime() < nextBlockTime) {
					newInput.primary = false;
					newInput.secondary = false;
				}
				if (newInput.secondary)
					newInput.primary = false;
				if (newInput.secondary != weapInput.secondary) {
					if (newInput.secondary) {
						if (IsBlockCursorActive()) {
							blockCursorDragging = true;
							blockCursorDragPos = blockCursorPos;
						} else {
							// cannot build; invalid position.
							if (listener && this == world.GetLocalPlayer()) {
								listener->LocalPlayerBuildError(
								  BuildFailureReason::InvalidPosition);
							}
						}
					} else {
						if (IsBlockCursorDragging()) {
							if (IsBlockCursorActive()) {
								std::vector<IntVector3> blocks =
								  GetWorld().CubeLine(blockCursorDragPos, blockCursorPos, 256);
								if ((int)blocks.size() <= blockStocks) {
									if (listener && this == world.GetLocalPlayer())
										listener->LocalPlayerCreatedLineBlock(blockCursorDragPos,
										                                      blockCursorPos);
									// blockStocks -= blocks.size(); decrease when created
								} else {
									// cannot build; insufficient blocks.
									if (listener && this == world.GetLocalPlayer()) {
										listener->LocalPlayerBuildError(
										  BuildFailureReason::InsufficientBlocks);
									}
								}
								nextBlockTime = world.GetTime() + GetToolSecondaryDelay();
							} else {
								// cannot build; invalid position.
								if (listener && this == world.GetLocalPlayer()) {
									listener->LocalPlayerBuildError(
									  BuildFailureReason::InvalidPosition);
								}
							}
						}

						blockCursorDragging = false;
						blockCursorActive = false;
					}
				}
				if (newInput.primary != weapInput.primary || newInput.primary) {
					if (newInput.primary) {

						if (!weapInput.primary)
							lastSingleBlockBuildSeqDone = false;
						if (IsBlockCursorActive() && blockStocks > 0) {
							if (listener && this == world.GetLocalPlayer())
								listener->LocalPlayerBlockAction(blockCursorPos, BlockActionCreate);

							lastSingleBlockBuildSeqDone = true;
							// blockStocks--; decrease when created

							nextBlockTime = world.GetTime() + GetToolPrimaryDelay();
						} else if (blockStocks > 0 && airborne && canPending &&
						           this == world.GetLocalPlayer()) {
							pendingPlaceBlock = true;
							pendingPlaceBlockPos = blockCursorPos;
						} else if (!IsBlockCursorActive()) {
							// wait for building becoming possible
						}

						blockCursorDragging = false;
						blockCursorActive = false;
					} else {
						if (!lastSingleBlockBuildSeqDone) {
							// cannot build; invalid position.
							if (listener && this == world.GetLocalPlayer()) {
								listener->LocalPlayerBuildError(
								  BuildFailureReason::InvalidPosition);
							}
						}
					}
				}
			} else if (IsToolWeapon()) {
				weapon->SetShooting(newInput.primary);

				// Update the weapon state asap so it picks up the weapon fire event even
				// if the player presses the mouse button and releases it really fast.
				// We shouldn't do this for the local player because the client haven't sent
				// a weapon update packet at this point and the hit will be rejected by the server.
				if (!IsLocalPlayer() && weapon->FrameNext(0.0f)) {
					FireWeapon();
				}
			} else {
				SPAssert(false);
			}

			weapInput = newInput;
		}

		void Player::Reload() {
			SPADES_MARK_FUNCTION();
			if (health == 0) {
				// dead man cannot reload
				return;
			}
			weapon->Reload();
			if (this == world.GetLocalPlayer() && weapon->IsReloading())
				reloadingServerSide = true;
		}

		void Player::ReloadDone(int clip, int stock) {
			reloadingServerSide = false;
			weapon->ReloadDone(clip, stock);
		}

		void Player::Restock() {
			SPADES_MARK_FUNCTION();
			if (health == 0) {
				// dead man cannot restock
				return;
			}

			weapon->Restock();
			grenades = 3;
			pendingRestockBlock = true;
			health = 100;

			if (world.GetListener())
				world.GetListener()->PlayerRestocked(*this);
		}

		void Player::GotBlock() {
			if (blockStocks < 50)
				blockStocks++;
		}

		void Player::SetTool(spades::client::Player::ToolType t) {
			SPADES_MARK_FUNCTION();

			if (t == tool)
				return;
			tool = t;
			holdingGrenade = false;
			blockCursorActive = false;
			blockCursorDragging = false;

			reloadingServerSide = false;

			WeaponInput inp;
			SetWeaponInput(inp);

			weapon->AbortReload();

			if (world.GetListener())
				world.GetListener()->PlayerChangedTool(*this);
		}

		void Player::SetHeldBlockColor(spades::IntVector3 col) { blockColor = col; }

		void Player::SetPosition(const spades::Vector3 &v) {
			SPADES_MARK_FUNCTION();

			position = v;
			eye = v;
		}

		void Player::SetVelocity(const spades::Vector3 &v) {
			SPADES_MARK_FUNCTION();

			velocity = v;
		}

		void Player::SetOrientation(const spades::Vector3 &v) {
			SPADES_MARK_FUNCTION();

			orientation = v;
		}

		void Player::Turn(float longitude, float latitude) {
			SPADES_MARK_FUNCTION();

			Vector3 o = GetFront();
			float lng = atan2f(o.y, o.x);
			float lat = atan2f(o.z, sqrtf(o.x * o.x + o.y * o.y));

			lng += longitude;
			lat += latitude;

			if (lat < -static_cast<float>(M_PI) * .49f)
				lat = -static_cast<float>(M_PI) * .49f;
			if (lat > static_cast<float>(M_PI) * .49f)
				lat = static_cast<float>(M_PI) * .49f;

			o.x = cosf(lng) * cosf(lat);
			o.y = sinf(lng) * cosf(lat);
			o.z = sinf(lat);
			SetOrientation(o);
		}

		void Player::SetHP(int hp, HurtType type, spades::Vector3 p) {
			health = hp;
			if (this == world.GetLocalPlayer()) {
				if (world.GetListener())
					world.GetListener()->LocalPlayerHurt(type,
					                                     p.x != 0.f || p.y != 0.f || p.z != 0.f, p);
			}
		}

		void Player::Update(float dt) {
			SPADES_MARK_FUNCTION();
			auto *listener = world.GetListener();

			MovePlayer(dt);

			if (!IsAlive()) {
				// do death cleanup
				blockCursorDragging = false;
			}

			if (tool == ToolSpade) {
				if (weapInput.primary) {
					if (world.GetTime() > nextSpadeTime) {
						UseSpade();
						nextSpadeTime = world.GetTime() + GetToolPrimaryDelay();
					}
				} else if (weapInput.secondary) {
					if (world.GetTime() > nextDigTime) {
						DigWithSpade();
						nextDigTime = world.GetTime() + GetToolSecondaryDelay();
						firstDig = false;
					}
				}
			} else if (tool == ToolBlock && IsLocalPlayer()) {
				GameMap::RayCastResult result;
				Handle<GameMap> map = GetWorld().GetMap();
				SPAssert(map);

				result = map->CastRay2(GetEye(), GetFront(), 12);
				canPending = false;

				if (blockCursorDragging) {
					// check the starting point is not floating
					auto start = blockCursorDragPos;
					if (map->IsSolidWrapped(start.x - 1, start.y, start.z) ||
					    map->IsSolidWrapped(start.x, start.y - 1, start.z) ||
					    map->IsSolidWrapped(start.x, start.y, start.z - 1) ||
					    map->IsSolidWrapped(start.x + 1, start.y, start.z) ||
					    map->IsSolidWrapped(start.x, start.y + 1, start.z) ||
					    map->IsSolidWrapped(start.x, start.y, start.z + 1)) {
						// still okay
					} else {
						// cannot build; floating
						if (listener && this == world.GetLocalPlayer()) {
							listener->LocalPlayerBuildError(BuildFailureReason::InvalidPosition);
						}
						blockCursorDragging = false;
					}
				}

				if (result.hit && (result.hitBlock + result.normal).z < 62 &&
				    (!OverlapsWithOneBlock(result.hitBlock + result.normal)) &&
				    BoxDistanceToBlock(result.hitBlock + result.normal) < 3.f &&
				    (result.hitBlock + result.normal).z >= 0 && !pendingPlaceBlock) {

					// Building is possible, and there's no delayed block placement.
					blockCursorActive = true;
					blockCursorPos = result.hitBlock + result.normal;

				} else if (pendingPlaceBlock) {

					// Delayed Block Placement: When player attempts to place a block while jumping
					// and
					// placing block is currently impossible, building will be delayed until it
					// becomes
					// possible, as long as player is airborne.
					if (airborne == false || blockStocks <= 0) {
						// player is no longer airborne, or doesn't have a block to place.
						pendingPlaceBlock = false;
						lastSingleBlockBuildSeqDone = true;
						if (blockStocks > 0) {
							// cannot build; invalid position.
						}
					} else if ((!OverlapsWithOneBlock(pendingPlaceBlockPos)) &&
					           BoxDistanceToBlock(pendingPlaceBlockPos) < 3.f) {
						// now building became possible.
						SPAssert(this == world.GetLocalPlayer());

						if (GetWorld().GetListener())
							GetWorld().GetListener()->LocalPlayerBlockAction(pendingPlaceBlockPos,
							                                                 BlockActionCreate);

						pendingPlaceBlock = false;
						lastSingleBlockBuildSeqDone = true;
						// blockStocks--; decrease when created

						nextBlockTime = world.GetTime() + GetToolPrimaryDelay();
					}

				} else {
					// Delayed Block Placement can be activated only when the only reason making
					// placement
					// impossible is that block to be placed overlaps with the player's hitbox.
					canPending = result.hit && (result.hitBlock + result.normal).z < 62 &&
					             BoxDistanceToBlock(result.hitBlock + result.normal) < 3.f;

					blockCursorActive = false;
					int dist = 11;
					for (; dist >= 1 && BoxDistanceToBlock(result.hitBlock + result.normal) > 3.f;
					     dist--) {
						result = GetWorld().GetMap()->CastRay2(GetEye(), GetFront(), dist);
					}
					for (; dist < 12 && BoxDistanceToBlock(result.hitBlock + result.normal) < 3.f;
					     dist++) {
						result = GetWorld().GetMap()->CastRay2(GetEye(), GetFront(), dist);
					}

					blockCursorPos = result.hitBlock + result.normal;
				}

			} else if (tool == ToolWeapon) {
			} else if (tool == ToolGrenade) {
				if (holdingGrenade) {
					if (world.GetTime() - grenadeTime > 2.9f) {
						ThrowGrenade();
					}
				}
			}

			if (tool != ToolWeapon)
				weapon->SetShooting(false);
			if (weapon->FrameNext(dt)) {
				FireWeapon();
			}

			if (weapon->IsReloading()) {
				lastReloadingTime = world.GetTime();
			} else if (reloadingServerSide) {
				// for some reason a server didn't return
				// WeaponReload packet.
				if (world.GetTime() + lastReloadingTime + .8f) {
					reloadingServerSide = false;
					weapon->ForceReloadDone();
				}
			}

			if (pendingRestockBlock) {
				blockStocks = 50;
				pendingRestockBlock = false;
			}
		}

		bool Player::RayCastApprox(spades::Vector3 start, spades::Vector3 dir) {
			Vector3 diff = position - start;

			// |P-A| * cos(theta)
			float c = Vector3::Dot(diff, dir);

			// |P-A|^2
			float sq = diff.GetPoweredLength();

			// |P-A| * sin(theta)
			float dist = sqrtf(sq - c * c);

			return dist < 8.f;
		}

		static float GetHorizontalLength(const Vector3 &v) {
			return std::sqrt(v.x * v.x + v.y * v.y);
		}

		enum class HitBodyPart { None, Head, Torso, Limb1, Limb2, Arms };

		void Player::FireWeapon() {
			SPADES_MARK_FUNCTION();

			Vector3 muzzle = GetEye();
			muzzle += GetFront() * 0.01f;

			// for hit-test debugging
			std::map<int, HitTestDebugger::PlayerHit> playerHits;
			std::vector<Vector3> bulletVectors;

			// Vector3 right = GetRight();
			// Vector3 up = GetUp();

			int pellets = weapon->GetPelletSize();
			float spread = weapon->GetSpread();
			Handle<GameMap> map = world.GetMap();

			SPAssert(map);

			if (!weapInput.secondary) {
				spread *= 2;
			}

			// pyspades takes destroying more than one block as a
			// speed hack (shotgun does this)
			bool blockDestroyed = false;

			// The custom state data, optionally set by `BulletHitPlayer`'s implementation
			std::unique_ptr<IBulletHitScanState> stateCell;

			Vector3 dir2 = GetFront();
			for (int i = 0; i < pellets; i++) {

				// AoS 0.75's way (dir2 shouldn't be normalized!)
				dir2.x += (SampleRandomFloat() - SampleRandomFloat()) * spread;
				dir2.y += (SampleRandomFloat() - SampleRandomFloat()) * spread;
				dir2.z += (SampleRandomFloat() - SampleRandomFloat()) * spread;
				Vector3 dir = dir2.Normalize();

				bulletVectors.push_back(dir);

				// first do map raycast
				GameMap::RayCastResult mapResult;
				mapResult = map->CastRay2(muzzle, dir, 500);

				stmp::optional<Player &> hitPlayer;
				float hitPlayerDistance = 0.f; // disregarding Z coordinate
				float hitPlayerActualDistance = 0.f;
				HitBodyPart hitPart = HitBodyPart::None;

				for (int i = 0; i < world.GetNumPlayerSlots(); i++) {
					// TODO: This is a repeated pattern, add something like
					//       `World::GetExistingPlayerRange()` returning a range
					auto maybeOther = world.GetPlayer(i);
					if (maybeOther == this || !maybeOther)
						continue;

					Player &other = maybeOther.value();
					if (!other.IsAlive() || other.GetTeamId() >= 2)
						continue;
					// quickly reject players unlikely to be hit
					if (!other.RayCastApprox(muzzle, dir))
						continue;

					HitBoxes hb = other.GetHitBoxes();
					Vector3 hitPos;

					if (hb.head.RayCast(muzzle, dir, &hitPos)) {
						float dist = GetHorizontalLength(hitPos - muzzle);
						if (!hitPlayer || dist < hitPlayerDistance) {
							hitPlayer = other;
							hitPlayerDistance = dist;
							hitPlayerActualDistance = (hitPos - muzzle).GetLength();
							hitPart = HitBodyPart::Head;
						}
					}
					if (hb.torso.RayCast(muzzle, dir, &hitPos)) {
						float dist = GetHorizontalLength(hitPos - muzzle);
						if (!hitPlayer || dist < hitPlayerDistance) {
							hitPlayer = other;
							hitPlayerDistance = dist;
							hitPlayerActualDistance = (hitPos - muzzle).GetLength();
							hitPart = HitBodyPart::Torso;
						}
					}
					for (int j = 0; j < 3; j++) {
						if (hb.limbs[j].RayCast(muzzle, dir, &hitPos)) {
							float dist = GetHorizontalLength(hitPos - muzzle);
							if (!hitPlayer || dist < hitPlayerDistance) {
								hitPlayer = other;
								hitPlayerDistance = dist;
								hitPlayerActualDistance = (hitPos - muzzle).GetLength();
								switch (j) {
									case 0: hitPart = HitBodyPart::Limb1; break;
									case 1: hitPart = HitBodyPart::Limb2; break;
									case 2: hitPart = HitBodyPart::Arms; break;
								}
							}
						}
					}
				}

				Vector3 finalHitPos;
				finalHitPos = muzzle + dir * 128.f;

				if (!hitPlayer && !mapResult.hit) {
					// might hit water surface.
				}

				if (mapResult.hit && GetHorizontalLength(mapResult.hitPos - muzzle) < 128.f &&
				    (!hitPlayer ||
				     GetHorizontalLength(mapResult.hitPos - muzzle) < hitPlayerDistance)) {
					IntVector3 outBlockCoord = mapResult.hitBlock;
					// TODO: set correct ray distance

					finalHitPos = mapResult.hitPos;

					if (outBlockCoord.x >= 0 && outBlockCoord.y >= 0 && outBlockCoord.z >= 0 &&
					    outBlockCoord.x < map->Width() && outBlockCoord.y < map->Height() &&
					    outBlockCoord.z < map->Depth()) {
						if (outBlockCoord.z == 63) {
							if (world.GetListener())
								world.GetListener()->BulletHitBlock(
								  mapResult.hitPos, mapResult.hitBlock, mapResult.normal);
						} else if (outBlockCoord.z == 62) {
							// blocks at this level cannot be damaged
							if (world.GetListener())
								world.GetListener()->BulletHitBlock(
								  mapResult.hitPos, mapResult.hitBlock, mapResult.normal);
						} else {
							int x = outBlockCoord.x;
							int y = outBlockCoord.y;
							int z = outBlockCoord.z;
							SPAssert(map->IsSolid(x, y, z));

							Vector3 blockF = {x + .5f, y + .5f, z + .5f};
							float distance = GetHorizontalLength(blockF - muzzle);

							uint32_t color = map->GetColor(x, y, z);
							int health = color >> 24;
							health -= weapon->GetDamage(HitTypeBlock, distance);
							if (health <= 0 && !blockDestroyed) {
								health = 0;
								blockDestroyed = true;
								// send destroy cmd
								if (world.GetListener() && world.GetLocalPlayer() == this)
									world.GetListener()->LocalPlayerBlockAction(outBlockCoord,
									                                            BlockActionTool);
							}
							color = (color & 0xffffff) | ((uint32_t)health << 24);
							if (map->IsSolid(x, y, z))
								map->Set(x, y, z, true, color);

							world.MarkBlockForRegeneration(outBlockCoord);

							if (world.GetListener())
								world.GetListener()->BulletHitBlock(
								  mapResult.hitPos, mapResult.hitBlock, mapResult.normal);
						}
					}
				} else if (hitPlayer) {
					if (hitPlayerDistance < 128.f) {

						finalHitPos = muzzle + dir * hitPlayerActualDistance;

						switch (hitPart) {
							case HitBodyPart::Head:
								playerHits[hitPlayer->GetId()].numHeadHits++;
								break;
							case HitBodyPart::Torso:
								playerHits[hitPlayer->GetId()].numTorsoHits++;
								break;
							case HitBodyPart::Limb1:
								playerHits[hitPlayer->GetId()].numLimbHits[0]++;
								break;
							case HitBodyPart::Limb2:
								playerHits[hitPlayer->GetId()].numLimbHits[1]++;
								break;
							case HitBodyPart::Arms:
								playerHits[hitPlayer->GetId()].numLimbHits[2]++;
								break;
							case HitBodyPart::None: SPAssert(false); break;
						}

						if (world.GetListener()) {
							switch (hitPart) {
								case HitBodyPart::Head:
									world.GetListener()->BulletHitPlayer(
									  *hitPlayer, HitTypeHead, finalHitPos, *this, stateCell);
									break;
								case HitBodyPart::Torso:
									world.GetListener()->BulletHitPlayer(
									  *hitPlayer, HitTypeTorso, finalHitPos, *this, stateCell);
									break;
								case HitBodyPart::Limb1:
								case HitBodyPart::Limb2:
									world.GetListener()->BulletHitPlayer(
									  *hitPlayer, HitTypeLegs, finalHitPos, *this, stateCell);
									break;
								case HitBodyPart::Arms:
									world.GetListener()->BulletHitPlayer(
									  *hitPlayer, HitTypeArms, finalHitPos, *this, stateCell);
									break;
								case HitBodyPart::None: SPAssert(false); break;
							}
						}
					}
				}

				if (world.GetListener())
					world.GetListener()->AddBulletTracer(*this, muzzle, finalHitPos);

				// one pellet done
			}

			// do hit test debugging
			auto *debugger = world.GetHitTestDebugger();
			if (debugger && IsLocalPlayer()) {
				debugger->SaveImage(playerHits, bulletVectors);
			}

			// in AoS 0.75's way
			Vector3 o = orientation;
			Vector3 rec = weapon->GetRecoil();
			float upLimit = Vector3::Dot(GetFront2D(), o);
			upLimit -= 0.03f; // ???

			// vanilla's horizontial recoil is driven by a triangular wave generator.
			int time = (int)(world.GetTime() * 1000);
			float triWave;
			if (time % 1024 < 512) {
				triWave = (time % 512) - 255.5;
			} else {
				triWave = 255.5 - (time % 512);
			}

			float horzModifier = 1;
			float vertModifier = 1;

			if ((input.moveLeft || input.moveRight || input.moveForward || input.moveBackward) && !weapInput.secondary) {
				horzModifier *= 2;
				vertModifier *= 2;
			}

			if (airborne) {
				horzModifier *= 2;
				vertModifier *= 2;
			}
			else if (input.crouch) {
				horzModifier /= 2;
				vertModifier /= 2;
			}

			horzModifier *= sqrt(1 - pow(o.z, 4));

			if (this->IsLocalPlayer()) {
				o += GetRight() * rec.x * triWave * horzModifier;
				o += GetUp() * std::min(rec.y, std::max(0.f, upLimit)) * vertModifier;
				o = o.Normalize();
				SetOrientation(o);
			}

			reloadingServerSide = false;
		}

		void Player::ThrowGrenade() {
			SPADES_MARK_FUNCTION();

			if (!holdingGrenade)
				return;
			grenades--;

			Vector3 muzzle = GetEye() + GetFront() * 0.1f;
			Vector3 vel = GetFront() * 1.f;
			float fuse = world.GetTime() - grenadeTime;
			fuse = 3.f - fuse;

			if (health <= 0) {
				// drop, don't throw
				vel = MakeVector3(0, 0, 0);
			}

			vel += GetVelocity();

			if (this == world.GetLocalPlayer()) {
				auto gren = stmp::make_unique<Grenade>(world, muzzle, vel, fuse);
				if (world.GetListener())
					world.GetListener()->PlayerThrewGrenade(*this, *gren);
				world.AddGrenade(std::move(gren));
			} else {
				// grenade packet will be sent by server
				if (world.GetListener())
					world.GetListener()->PlayerThrewGrenade(*this, {});
			}

			holdingGrenade = false;
		}

		void Player::DigWithSpade() {
			SPADES_MARK_FUNCTION();

			IntVector3 outBlockCoord;
			Handle<GameMap> map = world.GetMap();
			Vector3 muzzle = GetEye(), dir = GetFront();

			SPAssert(map);

			// TODO: set correct ray distance
			// first do map raycast
			GameMap::RayCastResult mapResult;
			mapResult = map->CastRay2(muzzle, dir, 256);

			outBlockCoord = mapResult.hitBlock;

			// TODO: set correct ray distance
			if (mapResult.hit && BoxDistanceToBlock(mapResult.hitBlock + mapResult.normal) < 3.f &&
			    outBlockCoord.x >= 0 && outBlockCoord.y >= 0 && outBlockCoord.z >= 0 &&
			    outBlockCoord.x < map->Width() && outBlockCoord.y < map->Height() &&
			    outBlockCoord.z < map->Depth()) {
				if (outBlockCoord.z < 62) {
					SPAssert(map->IsSolid(outBlockCoord.x, outBlockCoord.y, outBlockCoord.z));

					// send destroy command only for local cmd
					if (this == world.GetLocalPlayer()) {

						if (world.GetListener())
							world.GetListener()->LocalPlayerBlockAction(outBlockCoord,
							                                            BlockActionDig);
					}

					if (world.GetListener())
						world.GetListener()->PlayerHitBlockWithSpade(
						  *this, mapResult.hitPos, mapResult.hitBlock, mapResult.normal);
				}
			} else {
				if (world.GetListener())
					world.GetListener()->PlayerMissedSpade(*this);
			}
		}

		void Player::UseSpade() {
			SPADES_MARK_FUNCTION();

			bool missed = true;

			Vector3 muzzle = GetEye(), dir = GetFront();

			IntVector3 outBlockCoord;
			Handle<GameMap> map = world.GetMap();
			SPAssert(map);

			// TODO: set correct ray distance
			// first do map raycast
			GameMap::RayCastResult mapResult;
			mapResult = map->CastRay2(muzzle, dir, 256);

			stmp::optional<Player &> hitPlayer;
			int hitFlag = 0;

			for (int i = 0; i < world.GetNumPlayerSlots(); i++) {
				auto maybeOther = world.GetPlayer(i);
				if (maybeOther == this || !maybeOther)
					continue;

				Player &other = maybeOther.value();
				if (!other.IsAlive() || other.GetTeamId() >= 2)
					continue;
				if (!other.RayCastApprox(muzzle, dir))
					continue;
				if ((eye - other.GetEye()).GetChebyshevLength() >= MELEE_DISTANCE_F)
					continue;

				Vector3 diff = other.GetEye() - eye;
				Vector3 view;
				view.x = Vector3::Dot(diff, GetRight());
				view.y = Vector3::Dot(diff, GetUp());
				view.z = Vector3::Dot(diff, GetFront());

				if (view.z < 0.f)
					continue;

				view.x /= view.z;
				view.y /= view.z;
				view.z = 0.f;

				if (view.GetChebyshevLength() < 5.f) {
					hitPlayer = other;
					hitFlag = 1;
					break;
				}
			}

			outBlockCoord = mapResult.hitBlock;
			if (mapResult.hit && BoxDistanceToBlock(mapResult.hitBlock + mapResult.normal) < 3.f &&
			    !hitPlayer && outBlockCoord.x >= 0 && outBlockCoord.y >= 0 &&
			    outBlockCoord.z >= 0 && outBlockCoord.x < map->Width() &&
			    outBlockCoord.y < map->Height() && outBlockCoord.z < map->Depth()) {
				if (outBlockCoord.z < 62) {
					int x = outBlockCoord.x;
					int y = outBlockCoord.y;
					int z = outBlockCoord.z;
					SPAssert(map->IsSolid(x, y, z));
					missed = false;

					uint32_t color = map->GetColor(x, y, z);
					int health = color >> 24;
					health -= 55;
					if (health <= 0) {
						health = 0;
						// send destroy command only for local cmd
						if (this == world.GetLocalPlayer()) {
							if (world.GetListener())
								world.GetListener()->LocalPlayerBlockAction(outBlockCoord,
								                                            BlockActionTool);
						}
					}
					color = (color & 0xffffff) | ((uint32_t)health << 24);
					if (map->IsSolid(x, y, z))
						map->Set(x, y, z, true, color);

					world.MarkBlockForRegeneration(outBlockCoord);

					if (world.GetListener())
						world.GetListener()->PlayerHitBlockWithSpade(
						  *this, mapResult.hitPos, mapResult.hitBlock, mapResult.normal);
				}
			} else if (hitPlayer && world.GetListener() && hitFlag) {
				// The custom state data, optionally set by `BulletHitPlayer`'s implementation
				std::unique_ptr<IBulletHitScanState> stateCell;

				if (hitFlag)
					world.GetListener()->BulletHitPlayer(*hitPlayer, HitTypeMelee,
					                                     hitPlayer->GetEye(), *this, stateCell);
			}

			if (missed) {
				if (world.GetListener())
					world.GetListener()->PlayerMissedSpade(*this);
			}
		}

		Vector3 Player::GetFront() {
			SPADES_MARK_FUNCTION_DEBUG();
			return orientation;
		}

		Vector3 Player::GetFront2D() {
			SPADES_MARK_FUNCTION_DEBUG();
			return MakeVector3(orientation.x, orientation.y, 0.f).Normalize();
		}

		Vector3 Player::GetRight() {
			SPADES_MARK_FUNCTION_DEBUG();
			return -Vector3::Cross(MakeVector3(0, 0, -1), GetFront2D()).Normalize();
		}

		Vector3 Player::GetLeft() {
			SPADES_MARK_FUNCTION_DEBUG();
			return -GetRight();
		}

		Vector3 Player::GetUp() {
			SPADES_MARK_FUNCTION_DEBUG();
			return Vector3::Cross(GetRight(), GetFront()).Normalize();
		}

		bool Player::GetWade() {
			SPADES_MARK_FUNCTION_DEBUG();
			return GetOrigin().z > 62.f;
		}

		Vector3 Player::GetOrigin() {
			SPADES_MARK_FUNCTION_DEBUG();
			Vector3 v = eye;
			v.z += (input.crouch ? .45f : .9f);
			v.z += .3f;
			return v;
		}

		void Player::BoxClipMove(float fsynctics) {
			SPADES_MARK_FUNCTION();

			float f = fsynctics * 32.f;
			float nx = f * velocity.x + position.x;
			float ny = f * velocity.y + position.y;
			bool climb = false;
			float offset, m;
			if (input.crouch) {
				offset = .45f;
				m = .9f;
			} else {
				offset = .9f;
				m = 1.35f;
			}

			float nz = position.z + offset;

			float z;
			const Handle<GameMap> &map = world.GetMap();

			SPAssert(map);

			if (velocity.x < 0.f)
				f = -0.45f;
			else
				f = 0.45f;

			z = m;

			while (z >= -1.36f && !map->ClipBox(nx + f, position.y - .45f, nz + z) &&
			       !map->ClipBox(nx + f, position.y + .45f, nz + z))
				z -= 0.9f;
			if (z < -1.36f)
				position.x = nx;
			else if (!(input.crouch) && orientation.z < 0.5f && !input.sprint) {
				z = 0.35f;
				while (z >= -2.36f && !map->ClipBox(nx + f, position.y - .45f, nz + z) &&
				       !map->ClipBox(nx + f, position.y + .45f, nz + z))
					z -= 0.9f;
				if (z < -2.36f) {
					position.x = nx;
					climb = true;
				} else {
					velocity.x = 0.f;
				}
			} else {
				velocity.x = 0.f;
			}

			if (velocity.y < 0.f)
				f = -0.45f;
			else
				f = 0.45f;

			z = m;

			while (z >= -1.36f && !map->ClipBox(position.x - .45f, ny + f, nz + z) &&
			       !map->ClipBox(position.x + .45f, ny + f, nz + z))
				z -= 0.9f;
			if (z < -1.36f)
				position.y = ny;
			else if (!(input.crouch) && orientation.z < 0.5f && !input.sprint && !climb) {
				z = 0.35f;
				while (z >= -2.36f && !map->ClipBox(position.x - .45f, ny + f, nz + z) &&
				       !map->ClipBox(position.x + .45f, ny + f, nz + z))
					z -= 0.9f;
				if (z < -2.36f) {
					position.y = ny;
					climb = true;
				} else {
					velocity.y = 0.f;
				}
			} else if (!climb) {
				velocity.y = 0.f;
			}

			if (climb) {
				velocity.x *= .5f;
				velocity.y *= .5f;
				lastClimbTime = world.GetTime();
				nz -= 1.f;
				m = -1.35f;
			} else {
				if (velocity.z < 0.f)
					m = -m;
				nz += velocity.z * fsynctics * 32.f;
			}

			airborne = true;
			if (map->ClipBox(position.x - .45f, position.y - .45f, nz + m) ||
			    map->ClipBox(position.x - .45f, position.y + .45f, nz + m) ||
			    map->ClipBox(position.x + .45f, position.y - .45f, nz + m) ||
			    map->ClipBox(position.x + .45f, position.y + .45f, nz + m)) {
				if (velocity.z >= 0.f) {
					wade = position.z > 61.f;
					airborne = false;
				}
				velocity.z = 0.f;
			} else {
				position.z = nz - offset;
			}

			RepositionPlayer(position);
		}

		bool Player::IsOnGroundOrWade() {
			return ((velocity.z >= 0.f && velocity.z < .017f) && !airborne);
		}

		void Player::ForceJump() {
			velocity.z = -0.36f;
			lastJump = true;
			if (world.GetListener() && world.GetTime() > lastJumpTime + .1f) {
				world.GetListener()->PlayerJumped(*this);
				lastJumpTime = world.GetTime();
			}
		}

		void Player::MovePlayer(float fsynctics) {
			if (input.jump && (!lastJump) && IsOnGroundOrWade()) {
				velocity.z = -0.36f;
				lastJump = true;
				if (world.GetListener() && world.GetTime() > lastJumpTime + .1f) {
					world.GetListener()->PlayerJumped(*this);
					lastJumpTime = world.GetTime();
				}
			} else if (!input.jump) {
				lastJump = false;
			}

			float f = fsynctics;
			if (airborne)
				f *= 0.1f;
			else if (input.crouch)
				f *= 0.3f;
			else if ((weapInput.secondary && IsToolWeapon()) || input.sneak)
				f *= 0.5f;
			else if (input.sprint)
				f *= 1.3f;
			if ((input.moveForward || input.moveBackward) && (input.moveRight || input.moveLeft))
				f /= sqrtf(2.f);

			// looking up or down should alter speed
			const float maxVertLookSlowdown = 0.9f;
			const float vertLookSlowdownStart = 0.65f; // about 40 degrees
			float slowdownByVertLook =
			  std::max(std::abs(GetFront().z) - vertLookSlowdownStart, 0.0f) /
			  (1.0f - vertLookSlowdownStart) * maxVertLookSlowdown;

			Vector3 front = GetFront2D() * (1.0f - slowdownByVertLook);
			Vector3 left = GetLeft();

			if (input.moveForward) {
				velocity.x += front.x * f;
				velocity.y += front.y * f;
			} else if (input.moveBackward) {
				velocity.x -= front.x * f;
				velocity.y -= front.y * f;
			}
			if (input.moveLeft) {
				velocity.x += left.x * f;
				velocity.y += left.y * f;
			} else if (input.moveRight) {
				velocity.x -= left.x * f;
				velocity.y -= left.y * f;
			}

			// this is a linear approximation that's
			// done in pysnip
			// accurate computation is not difficult
			f = fsynctics + 1.f;
			velocity.z += fsynctics;
			velocity.z /= f; // air friction

			if (wade)
				f = fsynctics * 6.f + 1.f;
			else if (!airborne)
				f = fsynctics * 4.f + 1.f;

			velocity.x /= f;
			velocity.y /= f;

			float f2 = velocity.z;
			BoxClipMove(fsynctics);

			// hit ground
			if (velocity.z == 0.f && (f2 > FALL_SLOW_DOWN)) {
				velocity.x *= .5f;
				velocity.y *= .5f;

				if (f2 > FALL_DAMAGE_VELOCITY) {
					if (world.GetListener()) {
						world.GetListener()->PlayerLanded(*this, true);
					}
				} else {
					if (world.GetListener()) {
						world.GetListener()->PlayerLanded(*this, false);
					}
				}
			}

			if (velocity.z >= 0.f && velocity.z < .017f && !input.sneak && !input.crouch &&
			    !(weapInput.secondary && IsToolWeapon())) {
				// count move distance
				f = fsynctics * 32.f;
				float dx = f * velocity.x;
				float dy = f * velocity.y;
				float dist = sqrtf(dx * dx + dy * dy);
				moveDistance += dist * .3f;

				bool madeFootstep = false;
				while (moveDistance > 1.f) {
					moveSteps++;
					moveDistance -= 1.f;

					if (world.GetListener() && !madeFootstep) {
						world.GetListener()->PlayerMadeFootstep(*this);
						madeFootstep = true;
					}
				}
			}
		}

		bool Player::TryUncrouch() {
			SPADES_MARK_FUNCTION();

			float x1 = position.x + 0.45f;
			float x2 = position.x - 0.45f;
			float y1 = position.y + 0.45f;
			float y2 = position.y - 0.45f;
			float z1 = position.z + 2.25f;
			float z2 = position.z - 1.35f;

			const Handle<GameMap> &map = world.GetMap();

			SPAssert(map);

			// lower feet
			if (airborne && !(map->ClipBox(x1, y1, z1) || map->ClipBox(x2, y1, z1) ||
			                  map->ClipBox(x1, y2, z1) || map->ClipBox(x2, y2, z1))) {
				return true;
			} else if (!(map->ClipBox(x1, y1, z2) || map->ClipBox(x2, y1, z2) ||
			           map->ClipBox(x1, y2, z2) || map->ClipBox(x2, y2, z2))) {
				position.z -= 0.9f;
				eye.z -= 0.9f;
				return true;
			}
			return false;
		}

		void Player::RepositionPlayer(const spades::Vector3 &pos2) {
			SPADES_MARK_FUNCTION();

			eye = position = pos2;
			float f = lastClimbTime - world.GetTime();
			if (f > -.25f)
				eye.z += (f + .25f) / .25f;
		}

		float Player::GetToolPrimaryDelay() {
			SPADES_MARK_FUNCTION_DEBUG();

			switch (tool) {
				case ToolWeapon: return weapon->GetDelay();
				case ToolBlock: return .5f;
				case ToolSpade: return .2f;
				case ToolGrenade: return .5f;
				default: SPInvalidEnum("tool", tool);
			}
		}

		float Player::GetToolSecondaryDelay() {
			SPADES_MARK_FUNCTION_DEBUG();

			switch (tool) {
				case ToolBlock: return GetToolPrimaryDelay();
				case ToolSpade: return 1.f;
				default: SPInvalidEnum("tool", tool);
			}
		}

		float Player::GetSpadeAnimationProgress() {
			SPADES_MARK_FUNCTION_DEBUG();

			SPAssert(tool == ToolSpade);
			SPAssert(weapInput.primary);
			return 1.f - (nextSpadeTime - world.GetTime()) / GetToolPrimaryDelay();
		}

		float Player::GetDigAnimationProgress() {
			SPADES_MARK_FUNCTION_DEBUG();

			SPAssert(tool == ToolSpade);
			SPAssert(weapInput.secondary);
			return 1.f - (nextDigTime - world.GetTime()) / GetToolSecondaryDelay();
		}

		float Player::GetTimeToNextGrenade() { return nextGrenadeTime - world.GetTime(); }

		void Player::KilledBy(KillType type, Player &killer, int respawnTime) {
			SPADES_MARK_FUNCTION();
			health = 0;
			weapon->SetShooting(false);

			// if local player is killed while cooking grenade,
			// drop the live grenade.
			if (this == world.GetLocalPlayer() && tool == ToolGrenade && holdingGrenade) {
				ThrowGrenade();
			}
			if (world.GetListener())
				world.GetListener()->PlayerKilledPlayer(killer, *this, type);

			input = PlayerInput();
			weapInput = WeaponInput();
			this->respawnTime = world.GetTime() + respawnTime;
		}

		bool Player::IsAlive() { return health > 0; }

		std::string Player::GetName() { return world.GetPlayerPersistent(GetId()).name; }

		float Player::GetWalkAnimationProgress() {
			return moveDistance * .5f + (float)(moveSteps)*.5f;
		}

		Player::HitBoxes Player::GetHitBoxes() {
			SPADES_MARK_FUNCTION_DEBUG();
			Player::HitBoxes hb;

			Vector3 front = GetFront();

			float yaw = atan2(front.y, front.x) + static_cast<float>(M_PI) * .5f;
			float pitch = -atan2(front.z, sqrt(front.x * front.x + front.y * front.y));

			// lower axis
			Matrix4 lower = Matrix4::Translate(GetOrigin());
			lower = lower * Matrix4::Rotate(MakeVector3(0, 0, 1), yaw);

			Matrix4 torso;

			if (input.crouch) {
				lower = lower * Matrix4::Translate(0, 0, -0.4f);
				// lower
				hb.limbs[0] = AABB3(-.4f, -.15f, 0.5f, 0.3f, .3f, 0.5f);
				hb.limbs[0] = lower * hb.limbs[0];

				hb.limbs[1] = AABB3(.1f, -.15f, 0.5f, 0.3f, .3f, 0.5f);
				hb.limbs[1] = lower * hb.limbs[1];

				torso = lower * Matrix4::Translate(0, 0, -0.3f);

				// torso
				hb.torso = AABB3(-.4f, -.15f, 0.1f, .8f, .8f, .6f);
				hb.torso = torso * hb.torso;

				hb.limbs[2] = AABB3(-.6f, -.15f, 0.1f, 1.2f, .3f, .6f);
				hb.limbs[2] = torso * hb.limbs[2];

				// head
				hb.head = AABB3(-.3f, -.3f, -0.45f, .6f, .6f, 0.6f);
				hb.head = Matrix4::Translate(0, 0, -0.15f) * hb.head;
				hb.head = Matrix4::Rotate(MakeVector3(1, 0, 0), pitch) * hb.head;
				hb.head = Matrix4::Translate(0, 0, 0.15f) * hb.head;
				hb.head = torso * hb.head;
			} else {
				// lower
				hb.limbs[0] = AABB3(-.4f, -.15f, 0.f, 0.3f, .3f, 1.f);
				hb.limbs[0] = lower * hb.limbs[0];

				hb.limbs[1] = AABB3(.1f, -.15f, 0.f, 0.3f, .3f, 1.f);
				hb.limbs[1] = lower * hb.limbs[1];

				torso = lower * Matrix4::Translate(0, 0, -1.1f);

				// torso
				hb.torso = AABB3(-.4f, -.15f, 0.1f, .8f, .3f, .9f);
				hb.torso = torso * hb.torso;

				hb.limbs[2] = AABB3(-.6f, -.15f, 0.1f, 1.2f, .3f, .9f);
				hb.limbs[2] = torso * hb.limbs[2];

				// head
				hb.head = AABB3(-.3f, -.3f, -0.5f, .6f, .6f, 0.6f);
				hb.head = Matrix4::Translate(0, 0, -0.1f) * hb.head;
				hb.head = Matrix4::Rotate(MakeVector3(1, 0, 0), pitch) * hb.head;
				hb.head = Matrix4::Translate(0, 0, 0.1f) * hb.head;
				hb.head = torso * hb.head;
			}

			return hb;
		}
		IntVector3 Player::GetColor() { return world.GetTeam(teamId).color; }

		bool Player::IsCookingGrenade() { return tool == ToolGrenade && holdingGrenade; }
		float Player::GetGrenadeCookTime() { return world.GetTime() - grenadeTime; }

		Weapon &Player::GetWeapon() {
			SPADES_MARK_FUNCTION();
			SPAssert(weapon);
			return *weapon;
		}

		void Player::SetWeaponType(WeaponType weap) {
			SPADES_MARK_FUNCTION_DEBUG();
			if (this->weapon->GetWeaponType() == weap)
				return;
			this->weapon.reset(Weapon::CreateWeapon(weap, *this, *world.GetGameProperties()));
			this->weaponType = weap;
		}

		void Player::SetTeam(int tId) { teamId = tId; }

		bool Player::IsReadyToUseTool() {
			SPADES_MARK_FUNCTION_DEBUG();
			switch (tool) {
				case ToolBlock: return world.GetTime() > nextBlockTime && blockStocks > 0;
				case ToolGrenade: return world.GetTime() > nextGrenadeTime && grenades > 0;
				case ToolSpade: return true;
				case ToolWeapon: return weapon->IsReadyToShoot();
			}
		}

		bool Player::IsToolSelectable(ToolType type) {
			SPADES_MARK_FUNCTION_DEBUG();
			switch (type) {
				case ToolSpade: return true;
				case ToolBlock: return blockStocks > 0;
				case ToolWeapon: return weapon->GetAmmo() > 0 || weapon->GetStock() > 0;
				case ToolGrenade: return grenades > 0;
				default: SPAssert(false);
			}
		}

		bool Player::OverlapsWith(const spades::AABB3 &aabb) {
			SPADES_MARK_FUNCTION_DEBUG();
			float offset, m;
			if (input.crouch) {
				offset = .45f;
				m = .9f;
			} else {
				offset = .9f;
				m = 1.35f;
			}
			m -= .5f;
			AABB3 playerBox(eye.x - .45f, eye.y - .45f, eye.z, .9f, .9f, offset + m);
			return aabb && playerBox;
		}

		bool Player::OverlapsWithOneBlock(spades::IntVector3 vec) {
			SPADES_MARK_FUNCTION_DEBUG();
			return OverlapsWith(AABB3(vec.x, vec.y, vec.z, 1, 1, 1));
		}

#pragma mark - Block Construction
		bool Player::IsBlockCursorActive() { return tool == ToolBlock && blockCursorActive; }
		bool Player::IsBlockCursorDragging() { return tool == ToolBlock && blockCursorDragging; }
		float Player::BoxDistanceToBlock(spades::IntVector3 v) {
			Vector3 e = {(float)v.x, (float)v.y, (float)v.z};
			e += .5f;

			return (e - eye).GetChebyshevLength();
		}
	} // namespace client
} // namespace spades
